#!/usr/bin/env python


# import common stuff
from songanalysiscommon import *

#defines

n_date_analyzed = "user.songanalysis.dateAnalyzed" # a number which should be the stat() mtime entry
n_tempo = "user.songanalysis.tempo" # a number, expressed in python str(float(x)) format.  if nil, or inf or -inf, it should NOT BE STORED
n_songanalysis_interface = "user.songanalysis.interfaceVersion" # a number determining the interface version
n_freqbands = "user.songanalysis.frequencyBands" # a series of float numbers separated by commas.  if not available, it should NOT BE STORED
n_voldiff = "user.songanalysis.volumeDifference" # a float number.  if not available, it should NOT BE STORED

#end defines

from threading import Thread,RLock,Lock
from Queue import Empty
import os
import sys
import signal
import time
import errno
import logging
from sets import Set

# runtime tests
try:
	from extattr import getfattr,setfattr,delfattr
except:
	error = "The Smart DJ plug-in requires the python-extattr package to work properly.  Please install it and try again."
	say_sorry(error)
	sys.exit(0)
	
try:
	from commandsplus import shell_escape,getstatusoutputerror
except:
	error = "The Smart DJ plug-in requires the python-commandsplus package to work properly.  Please install it and try again."
	say_sorry(error)
	sys.exit(0)
	
# we import this after commandsplus to be sure call does not fail if commandsplus was not installed
from dcopamarok import dcop_amarok,DcopAmarokFailed

#librarize this version class FIXME
class Version:
	major = 0
	minor = 0
	micro = 0
	def __init__(self,version_string):
		splitted = version_string.split(".",2)
		try:
			major = int(splitted[0])
			try: minor = int(splitted[1])
			except IndexError: minor = 0
			try: micro = int(splitted[2])
			except IndexError: micro = 0
		except ValueError: raise ValueError, "invalid version string for Version(): %s"%version_string
		self.major = major
		self.minor = minor
		self.micro = micro
		self.version_string = version_string
		
	def __cmp__(self,other):
		for versiontuple in [ (self.major,other.major),(self.minor,other.minor),(self.micro,other.micro) ]:
			mine,others = versiontuple
			if mine > others: return 1
			if mine < others: return -1
		return 0

def test_songanalysis():
	# the songanalysis should spit its version on stdout
	minversion = "0.4.0"
	status,output,error = getstatusoutputerror("songanalysis -V")
	if status.command_not_found:
		return "The Smart DJ plug-in requires the songanalysis program, which has not been installed.  Please ensure songanalysis is correctly installed and in your PATH."
	if status.exited and status.return_value == 70:
		return "An old version of the songanalysis program is installed.  The Smart DJ plug-in requires at least version %s."%minversion
	if status.exited and status.return_value == 0:
		current = Version(output.strip())
		required = Version(minversion)
		if current >= required:
			return None # this means OKAY
		return "An old version of the songanalysis program is installed.  The Smart DJ plug-in requires at least version %s."%minversion
	return "An error occurred when checking for the songanalysis program (%s).  Please ensure songanalysis is correctly installed and in your PATH."%(status)

ret = test_songanalysis()
if ret:
	say_sorry(ret)
	sys.exit(0)


ui_available = False
try:
	import uiqt as songanalysisui
	ui_available = True
except ImportError:
	try:
		import uigtk as songanalysisui
		ui_available = True
	except ImportError: pass



# utility functions

def sql_escape(string):
	newstring = []
	for c in string:
		if c == "\\":
			newstring.append("\\\\")
		elif c == "'":
			newstring.append("\\'")
		else:
			newstring.append(c)
	return "'" + "".join(newstring) + "'"

def setup_logging():
# 	logging.basicConfig()
	f = file(os.path.expanduser(n_logfile),"a+b",0)
	handler = logging.StreamHandler(f)
	format = "%(levelname)s:%(name)s: %(message)s"
	formatter = logging.Formatter(format)
	handler.setFormatter(formatter)
	logging.getLogger().addHandler(handler)
	logging.getLogger().setLevel(logging.DEBUG)
	
# 	logger.debug("sys.argv: %s",sys.argv)
	if len(sys.argv) > 1 and sys.argv[1] == "-v":
		handler = logging.StreamHandler()
		handler.setFormatter(formatter)
		logging.getLogger().addHandler(handler)
	else:
		sys.stdout = f
		sys.stderr = f
# 		def log_exception(thetype,thevalue,traceback):
# 			print "MIERDA NOS HIZIMOS VERGA"
# 			from traceback import format_exception
# 			text = format_exception(  thetype, thevalue, traceback)
# 			logger.exception("Unhandled exception")
# 		sys.excepthook = log_exception
# 		sys.stdout = file("/dev/null","w")
# 		sys.stderr = file("/dev/null","w")
	
def finish_logging():
	logging.shutdown()

logger = logging.getLogger()

def passive_popup(message,title="Smart DJ",timeout=5):
	assert(type(title) is str)
	AsyncProcess(target=run,args=('kdialog','--title',title,'--passivepopup',message,str(timeout))).start()
	
def get_available_songanalysis_interface():
	r = getstatusoutputerror(["songanalysis","-N"])
	if r[0].exited and r[0].return_value == 0: return r[1].strip()
	raise Exception,str(r[0])

def playlist_popup(message):
	dcop_amarok("playlist","popupMessage",message)

def short_status_message(message):
	dcop_amarok("playlist","shortStatusMessage",message)
	
# end utility functions


# classes

class SongObject(object):
	def __init__(self,path,collection_expert):
		self._exec_query = collection_expert._exec_query
		self.path = path
		
	def __get_artist(self):
		if not hasattr(self,"_artist"):
			self._artist = self._exec_query("select artist.name from artist inner join tags on tags.artist = artist.id where tags.url = %s;"%sql_escape(self.path))
		return self._artist
	
	def __get_album(self):
		if not hasattr(self,"_album"):
			self._album = self._exec_query("select album.name from album inner join tags on tags.album = album.id where tags.url = %s;"%sql_escape(self.path))
		return self._album
	
	def __get_title(self):
		if not hasattr(self,"_title"):
			self._title = self._exec_query("select title from tags where tags.url = %s;"%sql_escape(self.path))
		return self._title
	
	def __get_playcount(self):
		if not hasattr(self,"_playcount"):
			try: self._playcount = int(self._exec_query("select playcounter from statistics where statistics.url = %s;"%sql_escape(self.path)))
			except ValueError,e: self._playcount = 0
		return self._playcount
	
	def __set_playcount(self,playcount):
		if self._exec_query("select url from statistics where statistics.url = %s;"%sql_escape(self.path)):
			self._exec_query("update statistics set playcounter = %d where statistics.url = %s;"%(playcount,sql_escape(self.path)))
		else:
			cdtime = int(round(time.time(),0))
			self._exec_query("insert into statistics (url,createdate,accessdate,percentage,playcounter) values(%s,%d,%d,%s,%d);"%(sql_escape(self.path),cdtime,cdtime,'NULL',playcount))
		self._playcount = playcount
	
	def __get_score(self):
		if not hasattr(self,"_score"):
			try: self._score = float(self._exec_query("select percentage from statistics where statistics.url = %s;"%sql_escape(self.path)))
			except ValueError,e: self._score = None
		return self._score
	
	def __set_score(self,score):
		if self._exec_query("select url from statistics where statistics.url = %s;"%sql_escape(self.path)):
			if score is None: dascore = "NULL"
			else: dascore = "%f"%score
			self._exec_query("update statistics set percentage = %s where statistics.url = %s;"%(dascore,sql_escape(self.path)))
		else:
			cdtime = int(round(time.time(),0))
			if score is None: dascore = "NULL"
			else: dascore = "%f"%score
			self._exec_query("insert into statistics (url,createdate,accessdate,percentage,playcounter) values(%s,%d,%d,%s,%d);"%(sql_escape(self.path),cdtime,cdtime,dascore,0))
		self._score = score
	
	def __get_tempo(self):
		if not hasattr(self,"_tempo"):
			try: self._tempo = float(self._exec_query("select bpm from analysis where analysis.url = %s;"%sql_escape(self.path)))
			except ValueError,e: self._tempo = None
		return self._tempo
	
	def __set_tempo(self,tempo):
		if self._exec_query("select url from analysis where analysis.url = %s;"%sql_escape(self.path)):
			self._exec_query("update analysis set bpm = %f where analysis.url = %s;"%(tempo,sql_escape(self.path)))
		else:
			songanalysis_interface_installed = get_available_songanalysis_interface()
			self._exec_query("insert into analysis (url,bpm,interface_used) values(%s,%f,%s);"%(
					sql_escape(self.path),tempo,sql_escape(songanalysis_interface_installed)))
	
	artist = property(__get_artist)
	title = property(__get_title)
	album = property(__get_album)
	playcount = property(__get_playcount,__set_playcount)
	score = property(__get_score,__set_score)
	tempo = property(__get_tempo,__set_tempo)


class CollectionExpert:
		
	logger = logging.getLogger("CollectionExpert")
	
	def __init__(self):
		self.analysis_priority_queue = []
		self.analysis_priority_callbacks = {}
		
	def add_to_priority_queue(self,filename,notify_callback=None): # we need to serialize this around a common lock X
		if not filename in self.analysis_priority_queue:
			self.logger.debug("Adding to priority queue: %s",filename)
			self.analysis_priority_queue.append(filename)
			if notify_callback:
				if not self.analysis_priority_callbacks.has_key(filename):
					self.analysis_priority_callbacks[filename] = []
				self.analysis_priority_callbacks[filename].append(notify_callback)
		
	def get_priority_queue_count(self):
		return len(self.analysis_priority_queue)
		
	def _exec_query(self,sql):
		# returns a string, sigh, because that's what the original implementation does
		self.logger.debug("Executing SQL query: %s",sql)
		r = dcop_amarok("collection","query",sql)
		if type(r) is list:
			return "\n".join(r)
		else: return r
		
	def prepare_database(self):
		self._exec_query(n_create_sql)
		
	def purge_songs_not_analyzed_with_interface(self,interface):
		sql1 = "delete from analysis where interface_used is NULL;"
		sql2 = "delete from analysis where interface_used != %s;"%sql_escape(interface)
		for a in [sql1,sql2]: self._exec_query(a)
		
	def get(self): # we need to serialize this around a common lock X
		try:
			pqe = self.analysis_priority_queue[0]
# 			self.logger.debug("Returning from priority queue: %s",pqe)
			return pqe
		except IndexError:
			sql = "select tags.url from tags left join analysis on (tags.url = analysis.url) where analysis.url is NULL limit 1;"
			# or analysis.interface_used != %s or analysis.interface_used is NULL limit 1
			response = self._exec_query(sql)
			if not response: raise Empty
			else: return response
	
	def get_current_song(self):
		try: return self.get()
		except Empty: return None
		
	def get_total_songs(self):
		sql = "select count(url) from tags as totalcount;"
		response = self._exec_query(sql)
		return int(response)
	
	def get_remaining_songs(self):
		sql = "select count(tags.url) as remaining from tags left join analysis on (tags.url = analysis.url) where analysis.url is NULL;"
		response = self._exec_query(sql)
		return int(response)
	
	def _delete_file(self,filename):
		sql = "delete from analysis where url = %s;"%sql_escape(filename)
		self._exec_query(sql)
	
	def associate(self,filename,tempo,freqbands,voldiff,songanalysis_interface_installed):
		
		self._delete_file(filename)
# 		
		def is_infinite(value):
			if value == float("inf"): return True
			if value == float("-inf"): return True
			if str(value) == "nan": return True
			return False
			
# 		print tempo,voldiff
# 		print type(tempo),type(voldiff)
		
		if tempo is None or is_infinite(tempo): tempo = "NULL"
		else: tempo = "%f"%tempo
		if voldiff is None or is_infinite(voldiff): voldiff = "NULL"
		else: voldiff = "%f"%voldiff
		if freqbands is None: freqbands = ",".join([ "NULL" for r in range(30) ])
		else:
			strfreqbands = []
			for f in freqbands:
				if f is None or is_infinite(f): strfreqbands.append("NULL")
				else: strfreqbands.append("%f"%f)
			freqbands = ",".join(strfreqbands)
		
		fnames = ",".join([ "freq%s"%s for s in range(30) ])
		sql = "insert into analysis (url,volume_diff,%s,bpm,interface_used) values(%s,%s,%s,%s,%s);" %(fnames,sql_escape(filename),voldiff,freqbands,tempo,sql_escape(songanalysis_interface_installed))
		
		
		self.logger.debug("SQL QUERY: %s",sql)
		self._exec_query(sql)
		
		# and we clean up our queues
		if filename in self.analysis_priority_queue:
			self.logger.debug("Removing from priority queue: %s",filename)
			self.analysis_priority_queue.remove(filename)
			if self.analysis_priority_callbacks.has_key(filename):
				self.logger.debug("Running callbacks on behalf of requestors")
				notify_callbacks = self.analysis_priority_callbacks[filename]
				for a in notify_callbacks:
					try: a(filename)
					except: self.logger.exception("Unhandled exception while running callback: %s"%a)
				self.logger.debug("Done running callbacks on behalf of requestors")
				del self.analysis_priority_callbacks[filename]
	
	def get_song(self,path):
		
		return SongObject(path,self)
	
	def get_similar_songs(self,filename,weight_freq,weight_bpm,limit,fixed_tempo=None,subquery=None):
		
		# FIXME revisar este algorithmo
		
		sql = "select * from analysis where url = %s;"% sql_escape(filename)
		output = self._exec_query(sql).splitlines()
		if not output:
			raise NotAnalyzedYet,filename
		if len(output) != 34:
			raise UnAnalyzable,"%s did not produce 34 output fields"%filename
		
		try:
			url,volume_diff,freqbands,bpm = output[0],float(output[1]),[ float(a) for a in output[2:32] ],float(output[32])
		except ValueError, e:
			raise UnAnalyzable,"some stored parameters in %s produced an error: %s"%(filename,output)

		
		def gen_volume_diff_sql (volume_diff):
			# return the relative difference in volume (positive means louder than the passed volume, negative means quieter)
			if volume_diff is None: volume_diff = "NULL"
			template = "volume_diff - %s"
			sql = template % (volume_diff)
			# we return unadjusted values
			return sql
		def gen_norm_volume_diff_sql (volume_diff):
			# normalized volume value, -1 ... 1, 1 has most similar volume
			s = gen_volume_diff_sql(volume_diff)
			s = "GREATEST( -1 , 1 - ABS( %s ) )"%s
			return s
		def gen_band_interference_sql(freqbands):
			# return the magnitude of the compound interference between frequency bands.  the larger the value, the most interference, the less compatible to the freqbands val passed.
		
			portions = []
			freq = freqbands
			
			band = 0
			template = "abs(%s - freq%s) + abs(%s - freq%s)"
			portions.append(template%(freq[band],band,freq[band],band+1))
			
			for band in range(1,29):
				template = "abs(%s - freq%s) + abs(%s - freq%s) + abs(%s - freq%s)"
				portions.append(template%(freq[band],band,freq[band],band-1,freq[band],band+1))
			
			band = 29
			template = "abs(%s - freq%s) + abs(%s - freq%s)"
			portions.append(template%(freq[band],band,freq[band],band-1))
			
			sql = " + ".join(portions)
			# 	sql = "1 - (( %s ) / 2.5 )"%sql
			# we return unadjusted values
			
			return sql
		def gen_norm_band_interference_sql(freqbands):
	# 		normalized interference value: -1...1, 1 has most similar freq spectrum distribution
			s = gen_band_interference_sql(freqbands)
			s = "1 - ( ( %s ) / 2.5)" % s
			s = "LEAST( GREATEST( %s , -1 ) , 1 )" %s
			return s
		def gen_compatibility_sql(volume_diff,freqbands):
			# this returns -1 ... 1 ranged values which take norm vol and norm interf. and sum them in a weighted sum 3/4 for the interference and 1/4 for the vol difference
			f = gen_norm_band_interference_sql(freqbands)
			v = gen_norm_volume_diff_sql(volume_diff)
			s = "0.75 * ( %s ) + 0.25 * ( %s )"%(f,v)
			return s
		def gen_bpm_diff_sql(bpm):
			if bpm is None: bpm = "NULL"
			sql = "bpm - %s"%bpm
			return sql
		def gen_norm_bpm_diff_sql(bpm):
			s = gen_bpm_diff_sql(bpm)
			s = "ABS( %s ) / (160 - 120)"%s  #MAX_BPM MIN_BPM ... how the fuck do they choose it?
			s = "1 - ( ( %s ) * 2 )"%s
			return s
		def gen_attraction_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm):
			c = gen_compatibility_sql(volume_diff,freqbands)
			b = gen_norm_bpm_diff_sql(bpm)
			s = "COALESCE( %s * %s , 0 ) + COALESCE( %s * %s , 0 )"%(c,weight_freq,b,weight_bpm)
			# we coalesce so if any value comes null, we simply sum zero to it
			return s
		def gen_force_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm):
			s = gen_attraction_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm)
			s = "ABS( %s ) * ( %s )"%(s,s) #we want to keep the sign that's why there is only one abs
			return s
		
		sql = "select COALESCE( tags.url , 'ERROR' ) as url , COALESCE( %s , -1 ) as force_value, COALESCE( bpm , 0) as tempo from analysis inner join tags on analysis.url = tags.url where volume_diff is not NULL %s order by force_value desc limit %s;"
		
		
		if fixed_tempo is not None: bpm = fixed_tempo
		if not subquery: subquery = ""
		else: subquery = "and tags.url in ( %s )"%subquery
		
		sql = sql%(gen_force_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm),subquery,limit)
# 		self.logger.debug("SQL about to be emitted: %r",sql)
		
# 		raise Exception,"stop"
		
		output = self._exec_query(sql)
		output = output.splitlines()
		assert ( len(output) / 3 ) * 3 == len(output)
		urls = [] ; forces = [] ; bpms = []
		while output:
			a = output.pop(0)
			urls.append(a)
			a = output.pop(0)
			forces.append(a)
			a = output.pop(0)
			bpms.append(a)
		results = []
		while urls and forces and bpms:
			results.append (    [    urls.pop(0) , float(forces.pop(0)) ,    float(bpms.pop(0))  ]      )
		return results
		





class NotSong(Exception): pass
class BackgroundSongAnalyzer(Thread):
	
	logger = logging.getLogger("SongAnalyzer")
	
	def __init__(self,collection_expert):
		Thread.__init__(self)
		self.collection_expert = collection_expert
		self.__finish = False
		
		
	def stop(self,async=False):
		self.__finish = True
		if not async:
			self.join()
	
	def run(self):
		
		try: self._run()
		except DcopAmarokFailed,e:
			self.logger.debug("Aborting because amaroK is unexpectedly gone: %s",e.output)
		except:
			self.logger.exception("Something wrong in background song analyzer, thread stopped")
			say_sorry("Something went wrong in the analyzer thread loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	def _run(self):
		analyzing_popup_shown = False
		done_popup_shown = False
		while self.__finish is False: #with this, we make this thread finish
			try:
				next_song = self.collection_expert.get()
				if analyzing_popup_shown is False:
					short_status_message("Smart DJ is analyzing your collection")
					analyzing_popup_shown = True
					done_popup_shown = False # we want to show this once a change in your collection is found
				self.process_song(next_song)
			except Empty:
				if done_popup_shown is False:
					short_status_message("Your music collection has been fully analyzed")
					done_popup_shown = True
					analyzing_popup_shown = False # the same goes for this
				self.logger.debug("sleeping")
				for seconds in range(30):
					if self.__finish is False: time.sleep(1)
				
			#we should rather simply sleep until the main thread signals that a rescan of the collection has just finished
		self.logger.debug("Analyzer done")
			
	def process_song(self,file):
		self.logger.info("Processing: " + file)
		
		# we begin by assuming we're analyzing the file and using eas
		onlyCollect = False
		useExtattrs = True
		
		#initialize vars we'll use
		tempo = None
		freqbands = None
		voldiff = None
		
		songanalysis_interface_installed = get_available_songanalysis_interface()
		# make something that if this value changes from function call N to function call M, the purge_songs_not_analyzed_with_interface function is called FIXME
		try: # Let's see if the file has songanalysis EAs
			date_analyzed  = getfattr(file,n_date_analyzed)
			
			#we'll also check if the EAs in the file were analyzed by the same songanalysis interface as the one we have installed
			try: songanalysis_interface_used  = getfattr(file,n_songanalysis_interface)
			except KeyError: songanalysis_interface_used = None
			self.logger.debug("Songanalysis interface available: %s     used for this song: %s"%(songanalysis_interface_installed,songanalysis_interface_used))
			
			if songanalysis_interface_used == songanalysis_interface_installed:
				# if the interfaces match, it's okay to collect from EAs
				self.logger.debug("A-OK, we only collect")
				onlyCollect = True # since the interfaces match and there is a date of analysis we will only collect
			else:
				# interface mismatch, a reanalysis is in order
				self.logger.debug("Interface mismatch, we will reanalyze")
				
		except KeyError: # File did not have an EA
			self.logger.debug("No last analysis date EA found, assuming no EA thus need to reanalyze and restore in the EA")
			
		except IOError, e:
				if e.errno == errno.EOPNOTSUPP: #No support for EAs on the filesystem where the file is installed
					onlyCollect = False # we will analyze the song
					useExtattrs = False # we won't store it on eas
				elif e.errno == errno.ENOENT:
					onlyCollect = False
					useExtattrs = False
					self.logger.debug("The song %s does not exist, letting the songanalysis process catch this so this file is marked as invalid"%file)
				else: raise
		
		if useExtattrs and onlyCollect: #if we're using already available eas
			self.logger.debug( "collecting info from extattrs")
			# we collect the info from the eas
			try: tempo = float(getfattr(file,n_tempo))
			except KeyError: pass
			try:
				freqbands = getfattr(file,n_freqbands)
				freqbands = freqbands.split(",")
				freqbands = [ float(f) for f in freqbands ]
			except KeyError: pass
			try: voldiff = float(getfattr(file,n_voldiff))
			except KeyError: pass
			self.logger.debug( "collected %s %s %s"%(tempo,freqbands,voldiff))
		else:
			process = AsyncProcess(self.analyze_song,(file,))
			process.start()
			
			# and periodically check whether it is done
			while self.__finish is False: # we periodically check whether we should abandon this
				try:
					tempo,freqbands,voldiff=process.get(block=True,timeout=1)
					break
				except NotReadyYet:
					continue
				except NotSong:
					# this is not a song, but we will mark it as NULL NULL NULL
					self.logger.debug( "Not a song!")
					break
		
			if self.__finish:
				self.logger.debug("shutdown requested, abandoning ongoing analysis")
				return
			
			if tempo != None and freqbands != None and voldiff != None:
				self.logger.debug( "deduced %s %s %s"%(tempo,freqbands,voldiff))
			
			if useExtattrs: # we store the info in eas
				try:
					for a in [ n_tempo,n_freqbands,n_voldiff,n_date_analyzed,n_songanalysis_interface]:
						try: delfattr(file,a) # let's wipe the attributes first
						except KeyError,e: pass # if there wasn't an attribute, go go gadget, ignore the evil!
					if tempo is not None: setfattr(file,n_tempo,str(tempo))
					if freqbands is not None: setfattr(file,n_freqbands,",".join([str(f) for f in freqbands]))
					if voldiff is not None: setfattr(file,n_voldiff,str(voldiff))
					date_analyzed = os.stat(file).st_mtime
					setfattr(file,n_date_analyzed,str(date_analyzed))
					setfattr(file,n_songanalysis_interface,songanalysis_interface_installed)
				except IOError,e:
					if e.errno == errno.EACCES: self.logger.debug( "no permission to save EAs for file %s, ignoring and moving on"%(file))
					elif e.errno == errno.ENOENT: self.logger.warning( "file %s vanished during analysis, saving in database, ignoring EA saves and moving on"%(file))
					else: raise
			
		# finally, we store the info in the DB
		self.store_analysis_info(file,tempo,freqbands,voldiff,songanalysis_interface_installed)
		
	def analyze_song(self,file):
		# return (tempo,freqbands,voldiff) (float,list of floats,float)
		
		self.logger.debug( "analyzing %s"%file)
		
		cmd = ["nice" ,"-n" ,"20" ,"songanalysis" ,file]
		status,output,error=getstatusoutputerror(cmd)
		
		if not status.exited:
			raise Exception, "songanalysis failed with status %s msg %s"%(status,error)
		if status.return_value == 70:
			raise NotSong, "%s is not a song understood by songanalysis, cannot be opened for reading or does not exist"%file

# 		self.logger.debug("analyzer spit out %s"%output)
		lines = [ l.strip() for l in output.splitlines() if l.strip() ]
		tuples = []
		for l in lines:
			key,val = l.split(":",1)
			tuples.append( (key.strip(),val.strip()) )
		
		tempo = None
		voldiff = None
		freqbands = None
		
		for t in tuples:
			key,val = t
			if key == "Volume diff": voldiff = float(val)
			if key == "Frequencies": freqbands = [ float(v) for v in val.split(" ") ]
			if key == "BPM": tempo = float(val)

		return tempo,freqbands,voldiff #if any of these is unavailable
		# we simply return None in that case
	
	def store_analysis_info(self,file,tempo,freqbands,voldiff,songanalysis_interface_installed):
		self.collection_expert.associate(file,tempo,freqbands,voldiff,songanalysis_interface_installed)


from Playlist import CurrentPlaylist
from SmartPlaylistCollection import SmartPlaylistCollection
class PlaylistExpert:
	logger = logging.getLogger("PlaylistExpert")
	
	def __init__(self):
		self.playlist_lock = Lock()
		self.smartplaylist_lock = Lock()
		self.__smartplaylistcollection = SmartPlaylistCollection()
		self.__currentplaylist = CurrentPlaylist()
	
	def get_active_index(self):
		try: return int(dcop_amarok("playlist","getActiveIndex"))
		except (TypeError,ValueError),e: return None
	
	def get_playing_path(self):
		path = dcop_amarok("player","path")
		if path: return path
	
	def get_playing_current_time(self): # in seconds
		try: return int(dcop_amarok("player","trackCurrentTime"))
		except (TypeError,ValueError),e: return None
	
	def set_playing_current_time(self,current_time): # int, in seconds
		return dcop_amarok("player","seek",str(current_time))
	
	def get_playing_total_time(self): # in seconds
		try: return int(dcop_amarok("player","trackTotalTime"))
		except (TypeError,ValueError),e: return None
	
	def get_total_track_count(self):
		return int(dcop_amarok("playlist","getTotalTrackCount"))

	def append_to_playlist(self,url):
		self.logger.debug("Adding %s to end of playlist",url)
		return dcop_amarok("playlist","addMedia",url)

	def play_now(self,url):
		self.logger.debug("Playing %s now",url)
		return dcop_amarok("playlist","playMedia",url)

	def get_smart_playlist_collection(self):
		self.smartplaylist_lock.acquire()
		try:
			self.__smartplaylistcollection.reread()
			return self.__smartplaylistcollection
		finally: self.smartplaylist_lock.release()

	def get_current_playlist(self):
		self.playlist_lock.acquire()
		try:
			self.__currentplaylist.reread()
			return self.__currentplaylist
		finally: self.playlist_lock.release()

class SuperDynamicModeMonitor(Thread,Queue):
		
	logger = logging.getLogger("SuperDynamicModeMonitor")
	
	def __init__(self,playlist_expert,collection_expert):
		Thread.__init__(self)
		Queue.__init__(self)
		self.__finish = False
		self.playlist_expert = playlist_expert
		self.collection_expert = collection_expert
		self.already_processed_files = Set()
		self.failed_cache = Set()
		self.songs_i_liked = []
		self.sdm_enabled = False
		self.loop_running = RLock()
		
	def stop(self,async=False):
		self.__finish = True
# 		self.notify("end")
		if not async:
			self.join()
	
	def run(self):
		try: self._run()
		except DcopAmarokFailed,e:
			self.logger.debug("Aborting because amaroK is unexpectedly gone: %s",e.output)
		except:
			self.logger.exception("Something wrong in Auto DJ mode monitor, process stopped")
			say_sorry("Something went wrong in the Auto DJ monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	def notify(self,event):
		self.put(event)
	
	def _run(self):
		self.on_start()
		while self.__finish is False: #with this, we make this process finish
			try: event = self.get(block=True,timeout=1)
			except Empty:
# 				self.logger.debug("No event received")
				continue
			self.logger.debug("Event received: %s",event)
			if event == "track_changed": self.on_track_changed()
			if event == "config_changed": self.on_config_changed()
			if isinstance(event,Exception): raise event
			
		self.on_end()
		self.logger.debug("Auto DJ Monitor bailed")

	def find_suitable_song(self,start_song,num_choices,spectrum_weight,tempo_weight,prune_songs,subquery=None):
		ce = self.collection_expert
		
		num_results = num_choices + 1 + len(prune_songs) + 10 # the +1 is because we know the start song will come first, and the +10 is because we'll prune songs with the same file name!
		
		self.logger.debug("Finding %s songs tempo/spectrum %s/%s",num_results,tempo_weight,spectrum_weight)
		results = ce.get_similar_songs(start_song,spectrum_weight,tempo_weight,num_results,subquery=subquery)
		results = [ r[0] for r in results ]
		
		if not results:
			self.logger.debug("Zero results from search, returning None")
			return None
		self.logger.debug("Search turned up %s raw results",len(results))

# 		prune_songs_cache = Set()
		if prune_songs:
			esequele = "select CONCAT( artist.name , ' - ' , tags.title ) from artist inner join tags on tags.artist = artist.id where tags.url in ( %s );" % " , ".join([ sql_escape(e) for e in prune_songs ])
			prune_songs_cache = [ s.lower() for s in ce._exec_query(esequele).splitlines() ]
		else:
			prune_songs_cache = []
			
	# 	self.logger.debug("Results: %s\n",results)
		self.logger.debug("We will be pruning %s paths",len(prune_songs))
		self.logger.debug("We will also be pruning %s song titles",len(prune_songs_cache))
		
		nuevos_elementos = []
		for result in results:
			if result in prune_songs:
				self.logger.debug("Pruning %s from results list",result)
				continue
			
			artistalbumstring = ce._exec_query("select CONCAT( artist.name , ' - ' , tags.title ) from artist inner join tags on tags.artist = artist.id where tags.url = %s ;" % sql_escape(result)).lower()
			
			if artistalbumstring in prune_songs_cache:
				self.logger.debug("Pruning %s from results list",artistalbumstring)
				continue
				
			nuevos_elementos.append(result)
			if len(nuevos_elementos) >= num_choices:
				self.logger.debug("Our pruned results list has achieved %s elements, breaking out of prune loop",num_choices)
				break
			
		results = nuevos_elementos
		if not results:
			self.logger.debug("Zero results after pruning, returning None")
			return None
		self.logger.debug("%s results after pruning",len(results))
		
		self.logger.debug("Randomly selecting among results")
		from random import choice
		return choice(results)


	def run_super_dynamic_loop(self): # Super Dynamic Loop!!!! TA-DAA!!!!!
		
		def run_if_unlocked():
			if not self.loop_running.acquire(blocking=0):
				return
			try:self._the_real_run()
			finally:self.loop_running.release()
		
		def run_guarded():
			try:
				run_if_unlocked()
			except DcopAmarokFailed,e:
				self.logger.debug("Aborting because amaroK is unexpectedly gone: %s",e.output)
			except Exception,e:
				self.logger.exception("Error while running super dynamic loop")
				self.notify(e)

		t = Thread(target=run_guarded)
		t.setDaemon(True)
		t.start()
		
	def _the_real_run(self):
		
		def count_upcoming_songs():
			active_index = self.playlist_expert.get_active_index()
			total_track_count = self.playlist_expert.get_total_track_count()
			if active_index is None: active_index = 0
			r = total_track_count - active_index - 1
			assert(r >= 0)
			return r
		
		sdm = get_config()["super_dynamic_mode"]
		
		if sdm["enabled"]:
			playing_song = self.playlist_expert.get_playing_path()
			if not playing_song in self.songs_i_liked:
				# let's check whether we need to add this to the list of songs you liked
				playing_song_current_time = self.playlist_expert.get_playing_current_time()
				playing_song_total_time = self.playlist_expert.get_playing_total_time() # in seconds
				if playing_song and playing_song_current_time and playing_song_total_time: # if there's a song playing
					playing_song_percent = float(playing_song_current_time) / float(playing_song_total_time)
					if playing_song_percent >= .40 or playing_song_current_time >= 60:
						self.logger.debug("We're liking %s, aren't we?"%playing_song)
						self.songs_i_liked.append(playing_song)
		
		now_upcoming_songs = count_upcoming_songs()
		if sdm["enabled"] and now_upcoming_songs < sdm["upcoming_songs"]:
			
			playlist_paths = [ a.Path for a in self.playlist_expert.get_current_playlist().tracks ]
		
			# we now choose the start path
			if sdm["choice_mechanism"] in [SONG_IM_LISTENING,SONG_IM_LISTENING_WANDER]:
				current_url = self.playlist_expert.get_playing_path()
			elif sdm["choice_mechanism"] == LAST_SONG_IN_PLAYLIST:
				if len(playlist_paths): current_url = playlist_paths[-1]
				else: current_url = None
			elif sdm["choice_mechanism"] == LAST_SONG_I_LIKED:
				# if the user skipped once, count_upcoming_songs() DEBE ser por fuerza 1 menos que sdm["upcoming_songs"], pero si el usuario ha saltado MAS de una cancion, entonces de ley esta condicion no se cumple
				if  now_upcoming_songs == sdm["upcoming_songs"] - 1 and now_upcoming_songs > 0:
					# hang on, unless the current url is the song he liked
					if playing_song in self.songs_i_liked:
						self.logger.debug("Upcoming song count is not critically low, but the user already decided this song he liked, so we're using it as search source")
						current_url = playing_song
					else:
						self.logger.debug("Upcoming song count is not critically low, forfeiting this chance")
						current_url = None
				else:
					if self.songs_i_liked:
						self.logger.debug("Upcoming song count is CRITICALLY low (or the user skipped more than once), using the last liked song as search start instead of hanging on waiting and seeing whether the user likes the current song")
						current_url = self.songs_i_liked[-1]
					else:
						self.logger.debug("Upcoming song count is CRITICALLY low, BUT there is no last liked song") # I wonder, should we fall back to currently being played URI?
						current_url = None
			else: assert(False)
			
			if not current_url:
				self.logger.debug("No start song, bailing")
				return # nothing's playing so we cannot determine what to use as criteria for start_url
			
			# we initialize the exceptions set with the already processed files list
			paths_to_prune = Set(self.already_processed_files)
			self.logger.debug("Initialized paths to prune with %s paths",len(paths_to_prune))
			paths_to_prune.union_update([ p for p in playlist_paths if p ])
			self.logger.debug("After adding paths in playlist, we're on %s paths",len(paths_to_prune))
			
			def get_subquery():
						# move part of this responsibility to SmartPlaylistCollection
				if not sdm["source_playlist"]: return None
				self.logger.debug("Using a subquery")
				try: subquery = self.playlist_expert.get_smart_playlist_collection()[sdm["source_playlist"]].sqlquery
				except KeyError, key:
					self.logger.warning("Smart playlist %s not found, resetting",key)
					sdm["source_playlist"] = None
					subquery = None
				return subquery
			
			source_playlist = sdm["source_playlist"]
			subquery = get_subquery()
			
			#FIXME this is for debuging
			start_time_2 = time.time()
			while sdm["enabled"] and count_upcoming_songs() < sdm["upcoming_songs"]:
				
				start_time = time.time()
				current_song = self.collection_expert.get_song(current_url)
				if source_playlist != sdm["source_playlist"]: # we need to reload the subquery
					source_playlist = sdm["source_playlist"] # remember the new source playlist
					subquery = get_subquery() # reset the subquery!

				self.logger.debug("Trying to find a similar song to %s",current_url)
				try:
					
# 					we add the path to the permanent and this run's list
					self.already_processed_files.add(current_url)
					paths_to_prune.add(current_url)
					
					if current_url not in self.failed_cache: # restrict showing of messages to tracks which did not fail in previous ocassions
						if sdm["top_choices"] == 1:
							short_status_message("Auto DJ mode is looking for a track similar to <b>%s</b> by <b>%s</b>"%(current_song.title,current_song.artist))
						else:
							short_status_message("Auto DJ mode is looking for %s tracks similar to <b>%s</b> by <b>%s</b>"%(sdm["top_choices"],current_song.title,current_song.artist))
					
					choice = self.find_suitable_song(
						current_url,sdm["top_choices"],
						sdm["spectrum_weight"],sdm["tempo_weight"],
						paths_to_prune,subquery=subquery)
					if choice:
						self.logger.debug("Choice: %s",choice)
						self.playlist_expert.append_to_playlist(choice)
						choice_song = self.collection_expert.get_song(choice)
# 						we add the path to the permanent and this run's list
						self.already_processed_files.add(choice)
						paths_to_prune.add(choice)
						short_status_message("Auto DJ mode added <b>%s</b> by <b>%s</b> to the playlist"%(choice_song.title,choice_song.artist))
						if sdm["choice_mechanism"] in [SONG_IM_LISTENING_WANDER,
							LAST_SONG_IN_PLAYLIST]:
								current_url = choice # yes, sir!
					else: raise NoChoicesFound
				except NotAnalyzedYet:
					if current_url not in self.failed_cache: # restrict showing of messages to tracks which did not fail in previous ocassions
						self.logger.warning("%s was not analyzed yet, adding to priority queue",current_url)
						self.collection_expert.add_to_priority_queue(current_url)
						short_status_message("Auto DJ mode is analyzing <b>%s</b> by <b>%s</b> to find similar tracks" 
							%(current_song.title,current_song.artist,))
						self.failed_cache.add(current_url)
					break
				except UnAnalyzable:
					self.logger.warning("%s could not be analyzed by songanalysis, breaking",current_url)
					if current_url not in self.failed_cache: # restrict showing of messages to tracks which did not fail in previous ocassions
# 						self.collection_expert.add_to_priority_queue(current_url)
						short_status_message("Auto DJ mode <b>was unable</b> to analyze <b>%s</b> by <b>%s</b>" 
							%(current_song.title,current_song.artist))
						self.failed_cache.add(current_url)
					break
				except NoChoicesFound:
					self.logger.warning("%s did not turn up any choices, breaking loop",current_url)
					if current_url not in self.failed_cache:
						short_status_message("Auto DJ mode <b>could not find</b> any tracks similar to <b>%s</b> by <b>%s</b>."
							%(current_song.title,current_song.artist))
						self.failed_cache.add(current_url)
					break
				self.logger.debug("Loop took %s seconds",time.time() -start_time)
			self.logger.debug("Total run took %s seconds",time.time() -start_time_2)
	
	def on_track_changed(self):
		self.run_super_dynamic_loop()
		
	def preserve_and_disable_amarok_config(self):
		m = self
		m.random_mode_status = dcop_amarok("player","randomModeStatus")
		m.dynamic_mode_status = dcop_amarok("player","dynamicModeStatus")
		dcop_amarok("player","enableRandomMode",False)
		dcop_amarok("player","enableDynamicMode",False)
		
	def restore_amarok_config(self):
		m = self
		if hasattr(m,"random_mode_status"):
			dcop_amarok("player","enableRandomMode",m.random_mode_status)
		if hasattr(m,"dynamic_mode_status"):
			dcop_amarok("player","enableDynamicMode",m.dynamic_mode_status)
	
	def on_config_changed(self):
		self.logger.debug("Auto DJ Monitor detected config change or just started up, adjusting everything")
		
		if get_config()["super_dynamic_mode"]["enabled"]:
			if not self.sdm_enabled:
				self.preserve_and_disable_amarok_config()
				self.logger.debug("Clearing already processed files and failed cache")
				self.already_processed_files = Set()
				self.failed_cache = Set()
				short_status_message("Auto DJ mode is now <b>on</b>.  Please <b>do not enable</b> Random or Dynamic modes.")
				self.sdm_enabled = True
				# we just got enabled
			self.run_super_dynamic_loop() # this needs to run whether we're just enabled or not, to correct any "imperfections" and adjust to the users' requests
		else:
			if self.sdm_enabled:
				# we just got disabled
				self.sdm_enabled = False
				self.restore_amarok_config()
				short_status_message("Auto DJ mode is now <b>off</b>.  Your Dynamic and Random settings have been restored.")
	
	def on_end(self):
		self.logger.debug("About to enter on_end")
		self.restore_amarok_config()
	
	def on_start(self):
		self.on_config_changed()
		
		def run_loop_every_twennay():
			while True:
				time.sleep(20)
				self.run_super_dynamic_loop()
		
		t = Thread(target=run_loop_every_twennay)
		t.setDaemon(True)
		t.start()



class MonitorContextBrowserJob(Thread):

	first = """
		function generateSongNode(url,similarity,title,tempo) {
			var row = document.createElement('tr');
			row.setAttribute('class','box-row');
			
			var cell = document.createElement('td');
			row.setAttribute('class','song');
			
//			var div = document.createElement('div');
//			div.setAttribute('class','album-song');
			
			var ahref = document.createElement('a');
			ahref.setAttribute('href',url);
			
			var songspan = document.createElement('span');
			songspan.setAttribute('class','album-song-title');
			
			var songtext = document.createTextNode(title);
			songspan.appendChild(songtext);
			
			ahref.appendChild(songspan);
			
//			div.appendChild(ahref);

			var cell2 = document.createElement('td');
			cell2.setAttribute('class','sbtext');
			cell2.setAttribute('width',1);
			var tempotext = document.createTextNode(""+tempo+" BPM");
			var temponobr = document.createElement('nobr');
			temponobr.appendChild(tempotext);
			cell2.appendChild(temponobr);

			var cell3 = document.createElement('td');
			cell3.setAttribute('class','sbtext');
			cell3.setAttribute('width',1);
			
			var snspan = document.createElement('span');
//			snspan.setAttribute('class','album-song-trackno');
			
			var sntext = document.createTextNode(similarity);
			snspan.appendChild(sntext);
			
			var snnobr = document.createElement('nobr');
			snnobr.appendChild(snspan);
			
			cell3.appendChild(snnobr);

// 			cell.appendChild(div);
 			cell.appendChild(ahref);
 			row.appendChild(cell);
			row.appendChild(cell2);
			row.appendChild(cell3);
			return row;
		}
			
		try {
			var refBox = document.getElementById('current_box');
			if (refBox != null) {
			
				var testAlreadyAdded = document.getElementById('similar_box');
				if (testAlreadyAdded != null) {
					testAlreadyAdded.parentNode.removeChild(testAlreadyAdded);
				}
				
				
				var songName = document.getElementById('current_box-header-songname');
				var songSep = document.getElementById('current_box-header-separator');
				var songArtist = document.getElementById('current_box-header-artist');
				var refBoxTable = document.getElementById('current_box-table');
		
				var similarBox = document.createElement('div');
				similarBox.setAttribute('id','similar_box');
				similarBox.setAttribute('class','box');
		
				var similarBoxHeader = document.createElement('div');
				similarBoxHeader.setAttribute('id','similar_box-header');
				similarBoxHeader.setAttribute('onclick','toggleBlock("T_ST"); window.location.href="togglebox:st";');
				similarBoxHeader.setAttribute('style','cursor: pointer;');
				similarBoxHeader.setAttribute('class','box-header');
		
				var similarBoxHeaderTitle = document.createElement('span');
				similarBoxHeaderTitle.setAttribute('class','box-header-title');
				similarBoxHeaderTitle.appendChild(document.createTextNode('Similar Tracks according to Smart DJ'));
				similarBoxHeader.appendChild(similarBoxHeaderTitle);
					
				similarBoxHeader.appendChild(document.createElement('br'));
				
				var similarBoxHeaderSubtitle = document.createElement('span');
				similarBoxHeaderSubtitle.setAttribute('class','box-header-title');
				similarBoxHeaderSubtitle.setAttribute('style','font-weight: normal;');
				similarBoxHeaderSubtitle.appendChild(document.createTextNode('%s'));
				similarBoxHeader.appendChild(similarBoxHeaderSubtitle);
				
		//		similarBoxHeader.appendChild(songName.cloneNode(true));
		//		similarBoxHeader.appendChild(document.createTextNode(' '));
		//		similarBoxHeader.appendChild(songSep.cloneNode(true));
		//		similarBoxHeader.appendChild(document.createTextNode(' '));
		//		similarBoxHeader.appendChild(songArtist.cloneNode(true));
		
				var similarBoxTable = refBoxTable.cloneNode(true);
				similarBoxTable.setAttribute('id','T_ST');
				similarBoxTable.setAttribute('class','box-body');
		
				var similarBoxTBody = similarBoxTable.firstChild
		
				while (similarBoxTBody.hasChildNodes()) {
					similarBoxTBody.removeChild(similarBoxTBody.firstChild);
				}
				
				%s
				
				similarBox.appendChild(similarBoxHeader);
				similarBox.appendChild(similarBoxTable);
		
				refBox.parentNode.insertBefore(similarBox,refBox.nextSibling);
			}
		} catch (e) {
			alert('An error occurred updating the context browser Current tab\\nThe following message box will contain more information.\\nPlease report this error to the Smart DJ developers.');
			alert(e);
		}
"""

	second = """
		var node =
		generateSongNode("%(url)s","%(number)s","%(title)s","%(bpm)s");
		similarBoxTBody.appendChild(node);
		"""

	logger = logging.getLogger("MonitorContextBrowserJob")

	def __init__(self,playlist_expert,collection_expert):
		Thread.__init__(self)
		path = os.path.dirname(dcop_amarok("playlist","saveCurrentPlaylist"))
		self.filename = os.path.join(path,"contextbrowser.html")
		self.logger.debug("Using %s as file for context browser",self.filename)
		self.flag_string = "<div id='current_box' class='box'>"
		self.collection_expert = collection_expert
		self.playlist_expert = playlist_expert
		self.execute_anyway = True
		self.__finish = False

	def stop(self,async=False):
		self.__finish = True
		if not async:
			self.join()
	
	def run(self):
# 		while not self.__finish: time.sleep(1)
		try: self._run()
		except DcopAmarokFailed,e:
			self.logger.debug("Aborting because amaroK is unexpectedly gone: %s",e.output)
		except Exception,e:
			self.logger.exception("Something wrong in context browser monitor, process stopped %s %s",e,e.__class__)
			say_sorry("Something went wrong in the context browser monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		except:
			self.logger.exception("Something wrong in context browser monitor, process stopped")
			say_sorry("Something went wrong in the context browser monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))

	def _run(self):
		old_mtime = None
		while self.__finish is False:
			time.sleep(1)
			if self.__finish: break
			mtime = os.stat(self.filename).st_mtime
			if (old_mtime and mtime != old_mtime) or self.execute_anyway:
				self.logger.debug("We are executing now!")
				self.execute_anyway = False
				time.sleep(0.3) # FIXME check if this affected negatively
				if self.__finish: break
				# we wait one second, but in reality we need to implement a synced loop or something to wait until the process has stabilized
				self.logger.debug("File stabilized, now reading") # ths is a lie
				htmlcontents = file(self.filename,"rb").read(-1)
				if self.flag_string in htmlcontents:
					self.logger.debug("file is current context browser, going forward with machiavellic plan")
					self.perform_js_injection()
				else:
					self.logger.debug( "file is not current context browser, ignoring" )
			old_mtime = mtime

	def notify_analysis_done(self,filename):
		self.logger.debug("Analysis for %s was completed, ahora si looking for similar songs",filename)
		self.execute_anyway = True
		
	def perform_js_injection(self):
		if self.__finish: return
		start_file = self.playlist_expert.get_playing_path()
		self.logger.debug("Using %s as start file",start_file)
		if start_file:
			if self.__finish: return
			c = get_config()["super_dynamic_mode"]
			try:
				if c["enabled"]:
					spectrum_weight,tempo_weight = (c["spectrum_weight"],c["tempo_weight"])
				else: spectrum_weight,tempo_weight = (1.0,1.0)
				weighting_text = "Criteria: %d%% tempo, %d%% music style"%(
						round(tempo_weight/2*100),round(spectrum_weight/2*100))
				results = self.collection_expert.get_similar_songs(start_file,spectrum_weight,tempo_weight,11)
			except NotAnalyzedYet,e:
				self.collection_expert.add_to_priority_queue(start_file,self.notify_analysis_done)
				start_song = self.collection_expert.get_song(start_file)
				short_status_message("Smart DJ is analyzing <b>%s</b> by <b>%s</b> to find similar songs"%
									(start_song.title,start_song.artist))
				self.logger.debug("%s will be ignored because it was not analyzed yet",start_file)
				return
			except UnAnalyzable,e:
				self.logger.debug("%s will be disregarded because its analysis failed",start_file)
				return
			if len(results) < 2:
				self.logger.debug( "No songs similar to %s found" ,start_file)
				return
# 			self.logger.debug("Results: %s",results)
			topcompat = max([ r[1] for r in results ])
			if results[0][0] == start_file: results = results[1:] # we peel off the first result if it's the same song
			paths = [ r[0] for r in results ]
			def trim_shit(string):
				if string.startswith(" - "): return string[3:]
				return string
			titles = []
			for path in paths:
				if self.__finish: return
				song = self.collection_expert.get_song(path)
				title = song.artist + " - " + song.title
				title = trim_shit(title)
				title = title.replace("\"","\\\"")
				titles.append(title)
			assert ( len(results) == len(titles) )
			from urllib import pathname2url
			urls = [ "file://" + pathname2url(p) for p in paths ]
			compats = [ r[1] for r in results ]
			percents = [ "%d"%(p*100.0/topcompat) +"%" for p in compats ]
			tempos = [ int(round(r[2])) for r in results ]
			parameters_list = [ { "url":urls[r] , "number": percents[r] , "title": titles[r] , "bpm" : tempos[r] } for r in range(len(results)) ]
			totalcommand = self.first%(weighting_text,"\n\n".join([ self.second%p for p in parameters_list ]))
			self.logger.debug("Submitting JavaScript") #: %s",totalcommand)
			if self.__finish: return
			dcop_amarok("html-widget2","evalJS",totalcommand)

		self.logger.debug("Context browser monitor done")


class AmarokSlaveMonitor(Thread):
		
	logger = logging.getLogger("SlaveMonitor")
	
	def __init__(self,playlist_expert,collection_expert,super_dynamic_mode_monitor):
		Thread.__init__(self)
		self.__finish = False
		self.playlist_expert = playlist_expert
		self.collection_expert = collection_expert
		self.super_dynamic_mode_monitor = super_dynamic_mode_monitor
		
	def stop(self,async=False):
		self.__finish = True
		if not async:
			self.join()
	
	def run(self):
		try: self._run()
		except DcopAmarokFailed,e:
			self.logger.debug("Aborting because amaroK is unexpectedly gone: %s",e.output)
		except Exception,e:
			self.logger.exception("Something wrong in monitor, process stopped %s %s",e,e.__class__)
			say_sorry("Something went wrong in the monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		except:
			self.logger.exception("Something wrong in monitor, process stopped")
			say_sorry("Something went wrong in the monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	
	def _run(self):
		import select
		import urllib
		po = select.poll()
		po.register(sys.stdin,select.POLLIN|select.POLLHUP|select.POLLERR|select.POLLPRI)
		while self.__finish is False: #with this, we make this process finish
			
# 			logger.debug("Parent PID: %s",os.getppid())
			if os.getppid() == 1: # we were reaped by INIT, shit!, parent is gone, we have to go!
				self.logger.debug("Parent is gone, forced exit")
				break
			
			try: readyfds = po.poll(1000) # wait one second
			except select.error,e:
				if e.args[0] == 4: continue # we were stopped, and now we're cont'ed
				else: raise
			if not readyfds: continue # spin because nothing good came out of this wait instance
				
			# or, if something good came out of it, read the line
			self.logger.debug("A new message from amaroK is in the queue")
# 			line = sys.stdin.readline().strip()
			try: line = sys.stdin.readline().strip()
			except IOError,e:
				if e.errno == 5:
					self.logger.debug("IOError 5, forced exit")
					break
				elif e.errno == 4: continue # we were stopped, and now we're cont'ed
				else: raise
			if line:
				# amarok sent us an event, here we do something useful
				self.logger.debug( "Processing: %s"%line)
				s = "customMenuClicked: Smart DJ Find tracks similar to selection..."
				t = "customMenuClicked: Smart DJ Set up Auto DJ mode..."
				u = "customMenuClicked: Smart DJ Analyze selected tracks now"
				v = "customMenuClicked: Smart DJ Edit tempo of selected tracks..."
				if line[:len(s)] == s:
					fnames = line[len(s)+1:]
					fnames = [ urllib.url2pathname(fname)[7:] for fname in fnames.split(" ") ]
					songanalysisui.search(fnames)
				elif line[:len(u)] == u:
					fnames = line[len(u)+1:]
					fnames = [ urllib.url2pathname(fname)[7:] for fname in fnames.split(" ") ]
					for fname in fnames:
						self.collection_expert.add_to_priority_queue(fname)
					if len(fnames) == 1:
						thesong = self.collection_expert.get_song(fnames[0])
						mm = "<b>%s</b> by <b>%s</b>" %(thesong.title,thesong.artist)
					else: mm = "<b>%s tracks</b>"%len(fnames)
					short_status_message("Added %s to analysis priority queue"%mm)
				elif line[:len(v)] == v:
					fnames = line[len(v)+1:]
					fnames = [ urllib.url2pathname(fname)[7:] for fname in fnames.split(" ") ]
					try: songanalysisui.edit_tempo(fnames)
					except AttributeError,e:
						say_sorry("The user interface Smart DJ is using right now does not support editing the tempo of a song yet.")
				elif line[:len(t)] == t or line == "configure":
					if ui_available:
						songanalysisui.show_configuration()
					else:
						say_sorry("To be able to configure Smart DJ and use Auto DJ, you must have a working installation of either PyQt (preferred) or PyGTK.")
				elif line == "trackChange":
					# track changed
					# if Auto DJ is enabled:
					# basically, if after the track i'm listening there are fewer tracks than what the user requested with the configuration for Auto DJ, then we should add as many tracks as the user requested
					if get_config()["super_dynamic_mode"]["enabled"]:
						self.super_dynamic_mode_monitor.notify("track_changed")
							
			else:
				# amarok sent us no event, probably because we received
				# a signal, so we just bail out of the loop
				self.logger.debug( "Empty message, bailing")
				break
		
		self.logger.debug("Monitor bailed")

# end classes


# begin config

_config = None

def load_config():
	logger.debug("loading config")
	global _config
	import pickle
	try:
		fn = os.path.expanduser(n_config)
		f = open(fn,"r")
		_config = pickle.load(f)
		f.close()
		logger.debug("loaded config successfully")
	except IOError, e: _config = None
	except EOFError,e: _config = None
	if _config is None:
		logger.debug("could not load config, using defaults")
		_config = dict() ; s = "super_dynamic_mode"
		_config[s] = dict()
		_config[s]["enabled"] = False
		_config[s]["top_choices"] = 1
		_config[s]["upcoming_songs"] = 3
		_config[s]["choice_mechanism"] = LAST_SONG_IN_PLAYLIST
		_config[s]["insert_position"] = LAST_SONG_IN_PLAYLIST
# 		_config[s]["tempo_spectrum_weights"] = (1.0,1.0)
	if not _config["super_dynamic_mode"].has_key("tempo_weight"):
		_config["super_dynamic_mode"]["tempo_weight"] = 1.0
	if not _config["super_dynamic_mode"].has_key("spectrum_weight"):
		_config["super_dynamic_mode"]["spectrum_weight"] = 1.0
	if not _config["super_dynamic_mode"].has_key("source_playlist"):
		_config["super_dynamic_mode"]["source_playlist"] = None

def save_config():
	logger.debug("saving config")
	global _config
	fn = os.path.expanduser(n_config)
	os.umask(077)
	try:
		f = open(fn,"w")
		import pickle
		pickle.dump(_config,f)
		f.close()
	except:
		logger.exception("could not save config, continuing")

def get_config():
	global _config
	return _config

_config_changed_handler = None
def notify_config_changed():
	global _config_changed_handler
	if _config_changed_handler: _config_changed_handler()

def set_config_changed_handler(handler):
	global _config_changed_handler
	_config_changed_handler = handler
	logger.debug("Setting config changed handler to %s",str(handler))

# end config


# public interface

class PublicInterface:
	def __init__(self,collection_expert_instance,playlist_expert_instance):
		for a in ["get_similar_songs","get_current_song","get_total_songs","get_remaining_songs","get_priority_queue_count","get_song"]:
			setattr(self,a,getattr(collection_expert_instance,a))
		for a in ["get_smart_playlist_collection","append_to_playlist","get_playing_path","get_active_index","get_playing_current_time","get_playing_total_time","set_playing_current_time","play_now"]:
			setattr(self,a,getattr(playlist_expert_instance,a))
		self.get_config = get_config
		self.notify_config_changed = notify_config_changed

#end public interface


#UI's
custom_menu_items = ["Find tracks similar to selection...","Edit tempo of selected tracks...","Analyze selected tracks now","Set up Auto DJ mode..."]
	
def enable_ui():
	logger.debug("enabling ui")
	for a in custom_menu_items:
		run("dcop","amarok","script","addCustomMenuItem","Smart DJ",a)
	
def disable_ui():
	logger.debug("disabling ui")
	for a in custom_menu_items:
		run("dcop","amarok","script","removeCustomMenuItem","Smart DJ",a)
	logger.debug("ui disabled")
	
def initialize_ui(public_interface):
	songanalysisui.initialize(public_interface)
	
#end UI's

# main function



class SignalQueue(Queue):
	def __init__(self):
		Queue.__init__(self)
	
	def handler(self,signal,frame):
		"""The installable signal handler"""
		logger.info( "received signal %s"%signal)
		self.put(signal)
		
	def trap(self,signal):
		from signal import signal as sigaction
		sigaction(signal,self.handler)


def _actual_main():

	
	pid = os.fork()
	if (pid): # the child, we go on as normal
		logger.debug("Parent process: forked.  PID: %s",os.getpid())
		signal_queue = SignalQueue()
		for sig in [signal.SIGINT,signal.SIGTERM]: signal_queue.trap(sig)
		while True:
			try: sig = signal_queue.get(block=True,timeout=1)
			except Empty:
				pid,status = os.waitpid(pid,os.WNOHANG)
				if pid:
					if os.WCOREDUMP(status):
						logger.debug("Child core dumped")
					elif os.WIFSIGNALED(status):
						logger.debug("Child got an unhandled signal %s",os.WTERMSIG(status))
					elif os.WIFEXITED(status):
						logger.debug("Child returned with return value %s",os.WEXITSTATUS(status))
					else:
						logger.debug("Child died with status %s",status)
					break
				continue
			logger.debug("Parent process got signal %s, going down and letting child notice on its own",sig)
# 			os.kill(pid,sig)
			break
		logger.debug("Parent process going down")
# 		while signal_queue.qsize() < 1:
# 			
# 			os.wait()
	else: # the parent, we exit now
		logger.debug("Child process: forked.  PID: %s",os.getpid())
	
		logger.info("starting up")
		load_config()
		
		logger.info("UI available: %s"%ui_available)
		
		collection_expert = CollectionExpert()
		playlist_expert = PlaylistExpert()
		
		collection_expert.prepare_database()
		collection_expert.purge_songs_not_analyzed_with_interface(get_available_songanalysis_interface()) #maybe this belongs in the BackgroundSongAnalyzer?
		analyzer = BackgroundSongAnalyzer(collection_expert)
		super_dynamic_mode_monitor = SuperDynamicModeMonitor(playlist_expert,collection_expert)
		monitor_context_browser_job = MonitorContextBrowserJob(playlist_expert,collection_expert)
		monitor = AmarokSlaveMonitor(playlist_expert,collection_expert,super_dynamic_mode_monitor)
		
		# setting up a signal handler - we should really turn this into signals and slots!!!!
		def generic_config_changed_handler():
			save_config()
			super_dynamic_mode_monitor.notify("config_changed")
			
		set_config_changed_handler(generic_config_changed_handler)
		
		signal_queue = SignalQueue()
		for sig in [signal.SIGINT,signal.SIGTERM]: signal_queue.trap(sig)
		
		logger.debug("starting analyzer")
		analyzer.start()
		logger.debug("starting Auto DJ monitor")
		super_dynamic_mode_monitor.start()
		logger.debug("starting context browser monitor")
		monitor_context_browser_job.start()
		logger.debug("starting monitor")
		monitor.start()
		
		logger.info("started")
		
		if ui_available:
			public_interface = PublicInterface(collection_expert,playlist_expert)
			initialize_ui(public_interface)
			enable_ui()
		else:
			passive_popup("You do not have either PyQt or PyGTK installed.\nTherefore, the functionality that requires an user interface won't be available.\nPlease install either PyQt (preferred) or PyGTK soon.")
		
		# now, we wait for impending doom
		while analyzer.isAlive() and monitor.isAlive() and super_dynamic_mode_monitor.isAlive() and monitor_context_browser_job.isAlive() and signal_queue.qsize() < 1: time.sleep(1)
		
		if ui_available: disable_ui()
		
		logger.debug("ending monitor")
		monitor.stop(async=True)
		logger.debug("ending context browser monitor")
		monitor_context_browser_job.stop(async=True)
		logger.debug("ending Auto DJ monitor")
		super_dynamic_mode_monitor.stop(async=True)
		logger.debug("ending analyzer")
		analyzer.stop(async=True)
		logger.debug("waiting for monitor")
		monitor.stop()
		logger.debug("waiting for context browser monitor")
		monitor_context_browser_job.stop()
		logger.debug("waiting for Auto DJ monitor")
		super_dynamic_mode_monitor.stop()
		logger.debug("waiting for analyzer")
		analyzer.stop()
		
		save_config()
		
		logger.info("ended")
	
	
def main():
	
	setup_logging()
	all_okay = True
	try: _actual_main()
# 	except SystemExit,e:
# 		if e == 0: sys.exit(0)
# 		else: raise
	except:
		all_okay = False
		logger.exception("An error occurred while starting up.  PID: %s",os.getpid())
		say_sorry("An error took place while starting up the Smart DJ plug-in.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
	
	finish_logging()
	if not all_okay: sys.exit(2)
	
	
if __name__ == "__main__":
	main()